Spring Security 프로젝트 설정 6 - JWT Refresh Token 생성 및 저장

✒️ 2025-05-28 14:24 내용 수정


Token 관리에 관한 고민


응답 클래스 수정

package com.example.security.auth;  
  
import com.fasterxml.jackson.annotation.JsonProperty;  
import lombok.AllArgsConstructor;  
import lombok.Builder;  
import lombok.Data;  
import lombok.NoArgsConstructor;  
  
@Data  
@Builder  
@AllArgsConstructor  
@NoArgsConstructor  
public class AuthenticationResponse {  
  
    @JsonProperty("access_token") // json에서 표기 수정  
    private String accessToken;  
  
    @JsonProperty("refresh_token")  
    private String refreshToken;  
}

Entity와 Repository 추가

  1. Refresh Token을 DB에 저장하기 위해 token 패키지를 생성하고, Token Entity를 생성한다.
    • DB에 저장할 Token에는 JWT와 Useremail을 저장한다.
package com.example.security.token;  
  
import jakarta.persistence.Entity;  
import jakarta.persistence.Id;  
import jakarta.persistence.Table;  
import lombok.AllArgsConstructor;  
import lombok.Builder;  
import lombok.Data;  
import lombok.NoArgsConstructor;  
  
@Data  
@Entity // Entity임을 명시  
@Builder // for Object building  
@NoArgsConstructor  
@AllArgsConstructor  
@Table(name = "token") // DB에 테이블 이름 지정  
public class Token {  
  
    @Id  
    private String refreshToken;  
    private String email;  
}
  1. 같은 패키지에 DB와 상호작용을 담당할 TokenRepository를 생성하고, accessToken으로 조회할 findByAccessToken() 메소드를 추가한다.
package com.example.security.token;  
  
import org.springframework.data.jpa.repository.JpaRepository;  
  
import java.util.Optional;  
  
public interface TokenRepository extends JpaRepository<Token, String> {  
  
    Optional<Token> findByRefreshToken(String token);  
}

Service

  1. JwtService 수정
    • JwtService에서 기존 Token 생성 메소드인 generateToken의 매개변수에 만료 기한인 long expireTime을 추가하고, Access Token과 Refresh Token을 생성하는 generateAccessToken() 메소드와 generateRefreshToken 메소드를 추가한다.
    • SECRET-KEY, accessTokenExpiration, refreshTokenExpiration를 애플리케이션의 설정 파일에 저장하고 그 값을 가져와 관리하기 위해 @Value Annotation을 사용하여 수정한다.
    • 아래 yml 파일에는 초 단위로 만료 기한을 지정했다.
      • accessTokenExpiration : 1000*60*60*24로 1일
      • refreshTokenExpiration : 1000*60*60*24*7로 7일
package com.example.security.config;  
  
import com.example.security.token.Token;  
import com.example.security.token.TokenRepository;  
import com.example.security.user.User;  
import io.jsonwebtoken.Claims;  
import io.jsonwebtoken.Jwts;  
import io.jsonwebtoken.SignatureAlgorithm;  
import io.jsonwebtoken.io.Decoders;  
import io.jsonwebtoken.security.Keys;  
import lombok.RequiredArgsConstructor;  
import org.springframework.beans.factory.annotation.Value;  
import org.springframework.security.core.userdetails.UserDetails;  
import org.springframework.stereotype.Service;  
  
import java.security.Key;  
import java.util.Date;  
import java.util.HashMap;  
import java.util.Map;  
import java.util.function.Function;  
  
@Service  
@RequiredArgsConstructor  
public class JwtService {  
    // DB와 상호작용하는 token repo    
    private final TokenRepository tokenRepository;  
  
    @Value("${app.security.jwt.secret-key}")  
    private String secretKey;  
  
    // Access Token 만료기한  
    @Value("${app.security.jwt.access-token-expiration}")  
    private long accessTokenExpiration;  
  
    // Refresh Token 만료기한  
    @Value("${app.security.jwt.refresh-token-expiration}")  
    private long refreshTokenExpiration;  
  
    // DB에 토큰 저장  
	public void saveUserToken(String refreshToken, User user) {  
	    Token token = new Token(refreshToken, user.getEmail());  
	    tokenRepository.save(token);  
	}

	// ...
  
    // Access 토큰 생성 - UserDetail로만 생성  
    public String generateAccessToken(UserDetails userDetails) {  
        return generateAccessToken(new HashMap<>(), userDetails);  
    }  
  
    // Access 토큰 생성  
    public String generateAccessToken(  
            Map<String, Object> extraClaims, // 토큰에 보낼 정보  
            UserDetails userDetails  
    ) {  
  
        return generateToken(extraClaims, userDetails, accessTokenExpiration);  
    }  
  
    // Refresh 토큰 생성 - UserDetail로만 생성  
    public String generateRefreshToken(UserDetails userDetails) {  
        return generateRefreshToken(new HashMap<>(), userDetails);  
    }  
  
    // Refresh 토큰 생성  
    public String generateRefreshToken(  
            Map<String, Object> extraClaims, // 토큰에 보낼 정보  
            UserDetails userDetails  
    ) {  
        return generateToken(extraClaims, userDetails, refreshTokenExpiration);  
    }  
  
    // 토큰 생성  
    private String generateToken(  
            Map<String, Object> extraClaims, // 토큰에 보낼 정보  
            UserDetails userDetails,  
            long expireTime  
    ) {  
        return Jwts  
                .builder()  
                .setClaims(extraClaims) // 클레임 추가  
                .setSubject(userDetails.getUsername()) // subject 추가  
                .setIssuedAt(new Date(System.currentTimeMillis())) // 토큰 발행일  
                .setExpiration(new Date(System.currentTimeMillis() + expireTime)) // 만료기한  
                .signWith(getSignInKey(), SignatureAlgorithm.HS256)  
                .compact();  
    }  
  
	// ... 
}
# app properties  
app:  
  security:  
    jwt:  
      secret-key: secret-key
      # 1일
      access-token-expiration: 86400000  
      # 7일
      refresh-token-expiration: 604800000
  1. AuthenticationService 수정
    • 사용자의 회원가입과 로그인 동작에서 Access Token과 Refresh Token을 각각 생성하고, 응답 객체에 두 Token을 같이 보낸다.
    • 이 때 생성한 Refresh Token은 DB에 저장하여 나중에 사용자가 Access Token 재발급을 요청할 때 전송한 Refresh Token과 비교한다.
package com.example.security.auth;  
  
import com.example.security.config.JwtService;  
import com.example.security.user.Role;  
import com.example.security.user.User;  
import com.example.security.user.UserRepository;  
import lombok.RequiredArgsConstructor;  
import org.springframework.security.authentication.AuthenticationManager;  
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;  
import org.springframework.security.crypto.password.PasswordEncoder;  
import org.springframework.stereotype.Service;  
  
@Service  
@RequiredArgsConstructor  
public class AuthenticationService {  
  
    // DB와 상호작용하는 사용자 repo    
    private final UserRepository repository;  
    // 비밀번호 인코더  
    private final PasswordEncoder passwordEncoder;  
    // jwt 서비스  
    private final JwtService jwtService;  
    // 사용자 신원 확인  
    private final AuthenticationManager authenticationManager;  
  
    // 회원가입  
    public AuthenticationResponse register(RegisterRequest request) {  
        // 요청으로부터 온 데이터로 사용자 객체 생성  
        var user = User.builder()  
                .firstname(request.getFirstname())  
                .lastname(request.getLastname())  
                .email(request.getEmail())  
                .password(passwordEncoder.encode(request.getPassword()))  
                .role(Role.USER)  
                .build();  
        // 사용자 저장  
        repository.save(user);  
  
        // 토큰 생성 - 사용자 정보로 생성  
        var accessToken = jwtService.generateAccessToken(user);  
        var refreshToken = jwtService.generateRefreshToken(user);  
  
        // 토큰을 db에 저장  
        jwtService.saveUserToken(refreshToken, user);  
  
        // 인증 응답 객체 생성  
        return AuthenticationResponse.builder()  
                .accessToken(accessToken)  
                .refreshToken(refreshToken)  
                .build();  
    }  
  
    // 인증 확인  
    public AuthenticationResponse authenticate(AuthenticationRequest request) {  
        // 요청으로 들어온 사용자의 신원 확인  
        authenticationManager.authenticate(  
                new UsernamePasswordAuthenticationToken(  
                        request.getEmail(),  
                        request.getPassword()  
                )  
        );  
        // 위의 인증을 거친 사용자를 DB에 검색  
        var user = repository.findByEmail(request.getEmail())  
                .orElseThrow();  
  
        // 토큰 생성 - 사용자 정보로 생성  
        var accessToken = jwtService.generateAccessToken(user);  
        var refreshToken = jwtService.generateRefreshToken(user);  
  
        // 토큰을 db에 저장  
        jwtService.saveUserToken(refreshToken, user);  
  
        // 인증 응답 객체 생성  
        return AuthenticationResponse.builder()  
                .accessToken(accessToken)  
                .refreshToken(refreshToken)  
                .build();  
    }  
}

Test

  1. 애플리케이션 실행 후 POSTMAN이나 Talent API에서 http://localhost:port/api/v1/auth/register로 회원가입 요청을 보낸 후 response를 확인하면 access_tokenrefresh_token을 확인할 수 있다.

spring_security_refresh_token 1.png

  1. 이번엔 http://localhost:port/api/v1/auth/authenticate로 로그인 요청을 보냈을 때 마찬가지로 access_tokenrefresh_token 응답을 확인할 수 있다.

spring_security_refresh_token 2.png

  1. DB를 실행하여 token 테이블에 데이터가 저장되었는지 확인하여 DB와도 연동이 잘 되는지 확인한다.
    • Token 저장은 session 저장용으로 많이 사용하는 Redis와도 연동해도 괜찮을 것 같다.

spring_security_refresh_token 3.png

  1. https://jwt.io/ 에서 access_tokenrefresh_token을 해독하여 사용자와 만료 기한이 잘 입력 되어 있는지 확인한다.

spring_security_refresh_token 4.png